package com.soupinabox;

import com.soupinabox.events.*;
import com.soupinabox.events.Event;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

/**
 * OCRTracker.java
 * <p/>
 * Author:  Taw
 * Date:    5/1/14
 */
public class OCRTracker extends Thread {

    private boolean running = false;

    private boolean useGameSource = false;

    private boolean skippedProcess = false;

    private boolean fadedIn = false;
    
    private Inventory inventory;

    private double avgMatchPercent=0,matchPercentSum=0;
    private int matchPercentCount=0;

    private Rectangle captureArea = null;

    /**
     * Last time data was processed
     */
    private long lastProcessTime = 0;
    /**
     * Last time fade was checked (not necessarily data processed)
     */
    private long lastCheckTime = 0;
    /**
     * Last time thresholds were optimized
     */
    private long lastOptimization = 0;
    /**
     * Time last item was added
     */
    private long lastItemTime = 0;

    private Robot robot;

    private static final double FLOOR_SCAN_Y = 0.9785, FLOOR_SCAN_MIN_WIDTH = 0.11, FLOOR_SCAN_MAX_WIDTH = 0.28, FLOOR_SCAN_INSET = 0.05;
    private double floorThreshold = 35;

    private static final double DESC_SCAN_MIN_WIDTH = 0.11, DESC_SCAN_MAX_WIDTH = 0.45, DESC_SCAN_INSET = 0.05;
    private double descThreshold;

    private static final double TOPBAR_SCAN_Y = .2085, TOPBAR_SCAN_MIN_WIDTH = 0.18, TOPBAR_SCAN_MAX_WIDTH = 0.28;
    private double topBarThreshold = 25;

    /**
     * The bounding rectangle of the current floor
     */
    private Rectangle floorRect = null;

    private WinDef.HWND hwnd;

    private RegionFrame regionFrame;


    private String lastItemAdded = "";
    private String lastItemRead = "";

    private ArrayList<String> needToAdd = new ArrayList<String>();

    public EventBroker broker;


    /**
     * Shitty debug variable names
     */
    BufferedImage debug_floorimg = null, debug_topbarimg = null, debug_descimg = null;
    String debug_floorstr="",debug_topbarstr="", debug_descstr="";

    JPanel debug_panel = new JPanel(){
        @Override
        public void paintComponent(Graphics g){
            super.paintComponent(g);
            if(debug_floorimg != null){
                g.drawImage(debug_floorimg,0,0,null);
                g.drawString(debug_floorstr,debug_floorimg.getWidth(),debug_floorimg.getHeight());
            }
            if(debug_topbarimg != null){
                g.drawImage(debug_topbarimg,0,100,null);
                g.drawString(debug_topbarstr,debug_topbarimg.getWidth(),50+debug_topbarimg.getHeight());
            }

            if(debug_descimg != null){
                g.drawImage(debug_descimg,0,75,null);
                g.drawString(debug_descstr,debug_descimg.getWidth(),75+debug_descimg.getHeight());
            }

            //g.drawImage(captureRegion(captureArea.width-captureArea.width*DESC_SCAN_MIN_WIDTH,captureArea.height*FLOOR_SCAN_Y,captureArea.width*DESC_SCAN_MIN_WIDTH,1,false),0,70,null);

            //g.drawImage(captureRegion(captureArea.width*.5,captureArea.height*TOPBAR_SCAN_Y,captureArea.width*TOPBAR_SCAN_MIN_WIDTH,10,false),0,50,null);
        }
    };


    
    public OCRTracker(EventBroker broker) {

        JFrame f = new JFrame("TRACKER DEBUG FRAME");

        f.setVisible(true);
        f.add(debug_panel);
        f.setSize(450,250);
        this.broker = broker;
        //TODO: Load up stuff...
        try{
            robot = new Robot(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice());
            hwnd = getGameWindow();
            if(hwnd != null ){
                //if the game is open, use the game window (until the option is included)
                useGameSource = true;
            }else{
                //Initialize the RegionFrame for screen region selection
                initRegionFrame();
            }
        }catch(AWTException e){
            e.printStackTrace();
        }
    }

    @Override
    public void start(){
        super.start();
        running=true;
    }

    public void run(){

        while(running){
            try{
                sleep(150);
                getCaptureArea();
                if(captureArea != null){
                    //Check whether the screen is even showing.
                    BufferedImage scanline = captureRegion(0,(int)(captureArea.height*FLOOR_SCAN_Y),captureArea.width*FLOOR_SCAN_MIN_WIDTH,1,false);
                    boolean black = true;
                    for(int i = 0; i < scanline.getWidth(); i++){
                        if(compareColors2(scanline.getRGB(i, 0), 0) > 20*20){
                            black = false;
                            break;
                        }
                    }

                    //I should redo this condition entirely......It was for the original code
                    if(!black && System.currentTimeMillis() - lastCheckTime > (skippedProcess && System.currentTimeMillis() - lastProcessTime < 5000 ? 100:400)){
                        lastCheckTime = System.currentTimeMillis();

                        float maxBrightness = 0;
                        boolean titleScreen = false;
                        for(int i = 0; i < scanline.getWidth(); i++){
                            //get the L* value from the L*a*b* color value of the pixel
                            int color = scanline.getRGB(i, 0);
                            if(i < scanline.getWidth()/2 && compareColors2(color,0xCC9999) < 100){
                                System.out.println("compare: "+compareColors2(color,0xCC9999));
                                titleScreen = true;
                                break;
                            }
                            maxBrightness = Math.max(toLab(color)[0], maxBrightness);
                        }
                        if(titleScreen)System.out.println("title screen");
                        if(maxBrightness > 65 && !titleScreen){
                            if(maxBrightness < 85 && !fadedIn){
                                //THE GAME IS PAUSED
                            }
                            lastProcessTime = System.currentTimeMillis();
                            //Fading is gone enough to allow for processing
                            if(fadedIn){
                                System.out.println("----FADE----");
                            }
                            if(fadedIn || System.currentTimeMillis() - lastOptimization > 15000){
                                lastOptimization = System.currentTimeMillis();
                                optimizeThresholds();
                            }

                            BufferedImage floorImage = getFloorImage(false);
                            int fcount = filterImage(floorImage,0xffffff,floorThreshold,0x000000);
                            String floor = "";
                            if(fcount > 20){
                                String fstring = processLetters(floorImage,0.5,3,1);
                                //System.out.println("FLOOR READ: " + fstring);
                                floor = matchFloorString(fstring);
                                debug_floorstr = floor;
                                //System.out.println("FLOOR MATCHED TO: " + floor);
                                if(floor.isEmpty()){
                                    optimizeThresholds();
                                    lastOptimization = System.currentTimeMillis();
                                }
                            }

                            String itemname= "";

                            if(checkForTopBar()){
                                BufferedImage topBarImage = getTopBarImage();
                                int tcount = filterImage(topBarImage,0xffffff,topBarThreshold,0x000000);
                                if(tcount > 20){
                                    String tstring = processLetters(topBarImage,0.75,10,2);
                                    System.out.println("TOP BAR READ: " + tstring);
                                    itemname = matchItemString(tstring);
                                    debug_topbarstr = itemname;
                                    System.out.println("TOP BAR MATCHED TO: " + itemname);
                                }
                                debug_topbarimg = topBarImage;
                            }else{
                                //for debugging
                                debug_topbarimg = null;
                                debug_topbarstr = "";
                            }


                            String descstring = "";
                            if(checkForDescription()){
                                //At least one white and one black pixel was detected, so we've found a description
                                BufferedImage descImage = getDescriptionImage();
                                int dcount = filterImage(descImage,0xffffff,descThreshold,0x000000);
                                if(dcount > 10){
                                    descstring = processLetters(descImage,0.75,10,2);
                                    System.out.println("DESC READ: " + descstring);
                                    //String desc = matchItemString(dstring);
                                    debug_descstr = descstring;
                                   // System.out.println("FLOOR MATCHED TO: " + desc);
                                    debug_descimg = descImage;
                                }
                            }else{
                                //for debugging
                                debug_descimg = null;
                                debug_descstr = "";
                            }

                            if(!itemname.isEmpty()){
                                if(itemname.equalsIgnoreCase("ODDMUSHROOM")){
                                    if(LevenshteinDistance(descstring,"FIRERATEUP",true)<4){
                                        itemname = "ODDMUSHROOMTHIN";
                                    }
                                }

                                if(!itemname.equals(lastItemRead) && !(itemname.equals(lastItemAdded) && System.currentTimeMillis()-lastItemTime < 750)){
                                    needToAdd.add(itemname);
                                    System.out.println("ITEM="+itemname);
                                    lastItemTime = System.currentTimeMillis();
                                }

                                lastItemRead = itemname;
                            }

                            if(needToAdd.size() > 0){
                                while(needToAdd.size()>0){
                                    String it =  needToAdd.remove(0);
                                    lastItemAdded = it;
                                    //TODO: ADD THIS ITEM
                                    Item ob=getItemObject(it);
                                    if(ob!=null)broker.fireEvent(new Event(EventType.ITEM_GAINED,ob.index));
                                }
                            }

                            debug_floorimg=floorImage;
                            debug_panel.repaint();

                            //TODO: Process the screen

                            fadedIn = false;
                        }else{
                            //The screen is no longer black, but it's not quite bright enough to be
                            //processed yet.
                            if(!titleScreen){
                                fadedIn = true;
                                System.out.println("-----2");
                            }
                        }
                    }else{
                        skippedProcess = true;
                        System.out.println("-----");
                    }
                }
            }catch (InterruptedException e){

            }
        }

    }

    public void initRegionFrame(){
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gd = ge.getDefaultScreenDevice();
        if(gd.isWindowTranslucencySupported(GraphicsDevice.WindowTranslucency.PERPIXEL_TRANSLUCENT)){
            regionFrame = new RegionFrame();

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {

                    regionFrame.setUndecorated(true);

                    regionFrame.setBackground(new Color(0, 0, 0, 0));

                    // Display the window.
                    regionFrame.setVisible(true);
                    regionFrame.requestFocus();
                    regionFrame.setAutoRequestFocus(true);
                }
            });
        }
    }


    public WinDef.HWND getGameWindow(){
        WinDef.HWND window = User32.INSTANCE.FindWindow("MDMZINC3WINDOW","Isaac");
        if(window == null){
            window =  User32.INSTANCE.FindWindow("TForm1","The Binding of Isaac: Wrath of the Lamb 1.48 (SpiderMod)");
        }
        return window;
    }

    public Rectangle getCaptureArea(){
        if(useGameSource && hwnd != null){
            WinUser.WINDOWINFO info = new WinUser.WINDOWINFO();
            User32.INSTANCE.GetWindowInfo(hwnd,info);
            WinDef.RECT rect = info.rcClient;
            if(Math.abs(rect.right-rect.left) > 0 && Math.abs(rect.bottom-rect.top) > 0){
                captureArea = new Rectangle(rect.left,rect.top,Math.abs(rect.right-rect.left),Math.abs(rect.bottom-rect.top));
            }else{
                captureArea = null;
            }
        }else if(regionFrame != null && regionFrame.REGION != null){
            captureArea = regionFrame.REGION;
        }
        return captureArea;
    }

    public int[] getRGBArray(BufferedImage i){
        return i.getRGB(0,0,i.getWidth(),i.getHeight(),null,0,i.getWidth());
    }


    public void optimizeThresholds(){
        System.out.println("Optimization start");
        float min = 1000;
        double best = 35, best2=35;
        for(double thresh = 20;thresh <= 60;thresh += 5){
            floorThreshold = thresh * thresh ;
            BufferedImage floorImage = getFloorImage(true);
            int fcount = filterImage(floorImage,0xffffff,floorThreshold,0x000000);
            if(fcount > 20){
                String fstring = processLetters(floorImage,0.5,3,1);
                float f = matchFloorDist(fstring);
                if(f < min){
                    min = f;
                    best = thresh;
                }
            }
        }

        best2=best;

        for(double thresh = best-2.5;thresh <= best+2.5; thresh += 1.25){
            floorThreshold = thresh*thresh;
            BufferedImage floorImage = getFloorImage(true);
            int fcount = filterImage(floorImage,0xffffff,floorThreshold,0x000000);
            if(fcount > 20){
                String fstring = processLetters(floorImage,0.5,3,1);
                float f = matchFloorDist(fstring);
                if(f < min){
                    min = f;
                    best2 = thresh;
                }
            }
        }
        floorThreshold = best2;
        topBarThreshold = floorThreshold*.85;
        descThreshold = floorThreshold*1.25;

        floorThreshold = floorThreshold*floorThreshold;
        topBarThreshold = topBarThreshold*topBarThreshold;
        descThreshold = descThreshold*descThreshold;

        System.out.println("optimized to: "+floorThreshold);
    }

    public boolean checkForTopBar(){
        //Use a scan line to check whether the top bar is being displayed
        boolean hitWhite = false, hitBlack = false;
        BufferedImage scanline = captureRegion(captureArea.width/2,captureArea.height*TOPBAR_SCAN_Y,captureArea.width*TOPBAR_SCAN_MIN_WIDTH,1,false);
        int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
        int count = 0;
        for(int i = 0; i < colors.length; i++){
            if(!hitWhite && compareColors2(colors[i],0xFFFFFF)<topBarThreshold){
                count++;
                if(count >= captureArea.width/175){
                            //captureArea.width/175 is just less than the minimum thickness of a letter in the top bar
                    hitWhite=true;
                    if(hitBlack)break;
                }
            }else if(!hitBlack && compareColors2(colors[i],0x000000)<100){
                hitBlack = true;
                if(hitWhite)break;
            }
        }
        return hitBlack && hitWhite;
    }

    public boolean checkForDescription(){
        //use a scanline to check whether a description is being displayed
        boolean hitWhite = false, hitBlack = false;
        BufferedImage scanline = captureRegion(captureArea.width-captureArea.width*(DESC_SCAN_MIN_WIDTH),captureArea.height*FLOOR_SCAN_Y,captureArea.width*(DESC_SCAN_MIN_WIDTH-DESC_SCAN_INSET),1,false);
        int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
        for(int i = colors.length-1; i >= 0; i--){
            if(!hitWhite && compareColors2(colors[i],0xFFFFFF)<descThreshold){
                hitWhite=true;
                if(hitBlack)break;
            }else if(!hitBlack && compareColors2(colors[i],0x000000)<100){
                hitBlack=true;
                if(hitWhite)break;
            }
        }

        return hitWhite && hitBlack;
    }


    public BufferedImage getFloorImage(boolean recalculate){

        if(!recalculate && floorRect != null && floorRect.width > 0 && floorRect.height > 0){
            return captureRegion(floorRect.x, floorRect.y, floorRect.width,floorRect.height,false);
        }
        //scanline to check whether anything is there
        int y = (int)(captureArea.height*FLOOR_SCAN_Y);
        int ymin = y;
        int ymax = y;
        boolean hitWhite;
        //find the top of the text region
        do{
            hitWhite = false;
            BufferedImage scanline = captureRegion(captureArea.width*FLOOR_SCAN_INSET,ymin,captureArea.width*(FLOOR_SCAN_MIN_WIDTH-FLOOR_SCAN_INSET),1,false);
            int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
            for(int i = 0; i < colors.length; i++){
                if(compareColors2(colors[i],0xFFFFFF)<floorThreshold){
                    hitWhite=true;
                    ymin--;
                    break;
                }
            }
        }while(hitWhite && ymin >= captureArea.height*0.85);
        if(ymin < y){
            //find the bottom of the text region
            do{
                hitWhite = false;
                BufferedImage scanline = captureRegion(captureArea.width*FLOOR_SCAN_INSET,ymax,captureArea.width*(FLOOR_SCAN_MIN_WIDTH-FLOOR_SCAN_INSET),1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                for(int i = 0; i < colors.length; i++){
                    if(compareColors2(colors[i],0xFFFFFF)<floorThreshold){
                        hitWhite=true;
                        ymax++;
                        break;
                    }
                }
            }while(hitWhite && ymax < captureArea.height);
            if(ymax-ymin > 6){
                //find the end of the text area.
                int lastwhite=0,lastblack=0;
                BufferedImage scanline = captureRegion(0,y,captureArea.width*FLOOR_SCAN_MAX_WIDTH,1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                double maxdist = ((ymax-ymin)/AVG_FONT_RATIO);
                for(int i = 0; i < colors.length && Math.min(i - lastwhite, i - lastblack) < maxdist*2.3; i++){
                    if(compareColors2(colors[i],0xFFFFFF)<floorThreshold){
                        lastwhite=i;
                    } else if (compareColors2(colors[i],0x000000)<100){
                        lastblack=i;
                    }
                }
                int width = Math.min(lastwhite,lastblack)+(int)maxdist+1;

                if(width > -1){
                    floorRect = new Rectangle(0,ymin-1,width+1,ymax-ymin+2);
                    return captureRegion(0,ymin-1,width+1,ymax-ymin+2,false);
                }
            }
        }
        floorRect = null;
        return null;
    }

    public BufferedImage getTopBarImage(){
        //scanline to check whether anything is there
        int y = (int)(captureArea.height*TOPBAR_SCAN_Y);
        int ymin = y;
        int ymax = y;
        boolean hitWhite;
        //find the top of the text region
        do{
            hitWhite = false;
            BufferedImage scanline = captureRegion(captureArea.width/2,ymin,captureArea.width*TOPBAR_SCAN_MIN_WIDTH,1,false);
            int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
            int count = 0;
            for(int i = 0; i < colors.length; i++){
                if(compareColors2(colors[i],0xFFFFFF)<topBarThreshold){
                    count++;
                    if(count >= captureArea.width/175){
                        hitWhite=true;
                        ymin--;
                        break;
                    }
                }
            }
        }while(hitWhite && ymin >= captureArea.height*0.15);
        if(ymin < y){
            //find the bottom of the text region
            do{
                hitWhite = false;
                BufferedImage scanline = captureRegion(captureArea.width/2,ymax,captureArea.width*TOPBAR_SCAN_MIN_WIDTH,1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                int count=0;
                for(int i = 0; i < colors.length; i++){
                    if(compareColors2(colors[i],0xFFFFFF)<topBarThreshold){
                        count++;
                        if(count>=captureArea.width/175){
                            hitWhite=true;
                            ymax++;
                            break;
                        }
                    }
                }
            }while(hitWhite && ymax < captureArea.height*0.25);
            if(ymax-ymin > 6){
                //find the end of the text area.
                int lastwhite=0,lastblack=0;
                BufferedImage scanline = captureRegion(captureArea.width/2,y,captureArea.width*TOPBAR_SCAN_MAX_WIDTH,1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                double maxdist = ((ymax-ymin)/AVG_FONT_RATIO);
                for(int i = 0; i < colors.length && Math.min(i-lastwhite,i-lastblack) < maxdist*2.3; i++){
                    //TODO: Deal with long stretches of aliased colors?
                    if(compareColors2(colors[i],0xFFFFFF)<topBarThreshold){
                        lastwhite=i;
                    } else if (compareColors2(colors[i],0x000000)<100){
                        lastblack=i;
                    }
                }
                int width = Math.min(lastwhite,lastblack)+(int)maxdist+1;

                //TODO: Figure out what went wrong here to make this only show the lower half of the image:
                //  return captureRegion(captureArea.width/2-width-1,ymin-1,width*2+2,ymax-ymin+2,false);

                //This works though...
                if(width > 0) {
                    return captureRegion(captureArea.width/2-width-1,ymin-2-(ymax-ymin)/2,width*2+2,(ymax-ymin)*2+2,false);
                }
            }
        }
        return null;
    }

    public BufferedImage getDescriptionImage(){
        //scanline to check whether anything is there
        int y = (int)(captureArea.height*FLOOR_SCAN_Y);
        int ymin = y;
        int ymax = y;
        boolean hitWhite;
        //find the top of the text region
        do{
            hitWhite = false;
            BufferedImage scanline = captureRegion(captureArea.width-captureArea.width*DESC_SCAN_MIN_WIDTH,ymin,captureArea.width*DESC_SCAN_MIN_WIDTH,1,false);
            int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
            for(int i = colors.length-1; i >= 0; i--){
                if(compareColors2(colors[i],0xFFFFFF)<descThreshold){
                    hitWhite=true;
                    ymin--;
                    break;
                }
            }
        }while(hitWhite && ymin >= captureArea.height*0.85);
        if(ymin < y){
            //find the bottom of the text region
            do{
                hitWhite = false;
                BufferedImage scanline = captureRegion(captureArea.width-captureArea.width*DESC_SCAN_MIN_WIDTH,ymax,captureArea.width*DESC_SCAN_MIN_WIDTH,1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                for(int i = colors.length-1; i >= 0; i--){
                    if(compareColors2(colors[i],0xFFFFFF)<descThreshold){
                        hitWhite=true;
                        ymax++;
                        break;
                    }
                }
            }while(hitWhite && ymax < captureArea.height);
            if(ymax-ymin > 6){
                //find the end of the text area.

                BufferedImage scanline = captureRegion(captureArea.width-captureArea.width*DESC_SCAN_MAX_WIDTH,ymin,captureArea.width*DESC_SCAN_MAX_WIDTH,1,false);
                int[] colors = scanline.getRGB(0,0,scanline.getWidth(),scanline.getHeight(),null,0,scanline.getWidth());
                double maxdist = ((ymax-ymin)/AVG_FONT_RATIO);
                int lastblack=colors.length-1;
                for(int i = colors.length - 1; i >= 0  && lastblack-i < 4; i--){
                    if (compareColors2(colors[i],0x000000)<100){
                        lastblack=i;
                    }
                }

                int width = colors.length - lastblack - 3;
                if(width > 0) {
                    return captureRegion(captureArea.width-width-1,ymin-1,width+2,ymax-ymin+2,false);
                }
            }
        }
        return null;
    }


    /**
     * Converts an image to black and white only, where any pixels within a <tt>threshold</tt> difference of <tt>acceptColor</tt> is colored black, and anything else is white.
     * The image passed into the command is set
     *
     * @param img image to filter. This image will be overwritten with the filtered data.
     * @param acceptColor the color to be accepted as foreground
     * @param threshold threshold value below which a color difference is assumed "close enough"
     * @param ignoreColor RGB value of a background color to ignore, just to skip those pixels for efficienct
     * @return the number of accepted (black) pixels in the filtered image
     */
    public int filterImage(BufferedImage img, int acceptColor, double threshold, int ignoreColor){
        if(img == null)return 0;
        int count=0;
        int[] colors = new int[img.getWidth()*img.getHeight()];
        img.getRGB(0,0,img.getWidth(),img.getHeight(),colors,0,img.getWidth());
        for(int i=0;i<colors.length;i++){
            if((colors[i]&0xffffff) != ignoreColor && compareColors2(acceptColor,colors[i]) < threshold){
                count++;
                colors[i]=0xff000000;
            }else{
                colors[i]=0xffffffff;
            }
        }
        img.setRGB(0, 0, img.getWidth(), img.getHeight(), colors, 0, img.getWidth());
        return count;
    }


    /**
     * Returns an estimated difference between the two colors, 2.3 is approximately one "Just Noticeable Difference" value
     *
     * @param c1 RGB value
     * @param c2 RGB value
     * @return estimated difference between c1 and c2
     */
    public static double compareColors(int c1,int c2){
        float f1[] = toLab(c1);
        float f2[] = toLab(c2);
        return Math.sqrt(floatSq(f1[0]-f2[0])+floatSq(f1[1]-f2[1])+floatSq(f1[2]-f2[2]));
    }

    public static double compareColors2(int c1,int c2){
        //compareColors() without the sqrt, for efficiency. All values checked against this should be squared first.
        float f1[] = toLab(c1);
        float f2[] = toLab(c2);
        return floatSq(f1[0]-f2[0])+floatSq(f1[1]-f2[1])+floatSq(f1[2]-f2[2]);
    }

    public static float floatSq(float f1){return f1*f1;}


    public static float[] toLab(int color){
        return toLab(new Color(color));
    }

    public static float[] toLab(Color color){
        //Convert RGB color to L*a*b* color space. Used for color difference calculations.
        return color.getColorComponents(CIELab.getInstance(), new float[3]);
    }


    public BufferedImage captureRegion(double x,double y,double w,double h, boolean ratios){
        Rectangle r = null;
        if(captureArea != null){
            if(ratios){
                r = new Rectangle(captureArea.x+(int)(captureArea.width*x),captureArea.y+(int)(captureArea.height*y),(int)(captureArea.width*w),(int)(captureArea.height*h));
            }else{
                r = new Rectangle(captureArea.x+(int)x,captureArea.y+(int)y,(int)w,(int)h);
            }
            //System.out.println(r);
            return robot.createScreenCapture(r);
        }
        return null;
    }


    /**
     * Count how many vertically-split regions there are in a single image without actually generating new images
     * <p>
     * Use this when it doesn't matter what the actual split images look like.
     *
     * @param img image to be split
     * @param minArea minimum area to consider a region to be countable
     * @param minWidth minimum width to consider a region to be countable
     * @return number of individual regions in the given image
     */
    public int splitCount(BufferedImage img, int minArea, int minWidth){
        int count=0;
        int area=0;
        boolean lastColumnEmpty = true;
        int startCol = 0;
        for(int x=0;x<img.getWidth();x++){
            boolean columnHasData = false;
            for(int y=0;!(columnHasData && area>=minArea) && y<img.getHeight();y++){
                if((img.getRGB(x,y)&0xffffff) != 0xffffff){
                    if(lastColumnEmpty){
                        startCol = x;
                        area = 0;
                    }
                    lastColumnEmpty=false;
                    columnHasData = true;
                    area++;
                }else if(!lastColumnEmpty && y == img.getHeight()-1){
                    lastColumnEmpty = true;
                    if(x-startCol >= minWidth && area >= minArea){
                        count++;
                    }
                }
            }
        }
        return count;
    }


    /**
     * Subdivide a B/W image into sub-images based on vertical gaps
     *
     * @param img image to be split
     * @param minArea minimum number of pixels required for an image segment to be considered a possible sub-image
     * @param minWidth minimum width required for an image segment to be considered a possible sub-image 
     * @return ArrayList of sub-images
     */
    public ArrayList<BufferedImage> splitImage(BufferedImage img, int minArea, int minWidth){

        //TODO: HEAVILY COMMENT THIS AND/OR ENTIRELY REWRITE IT
        //So much spaghetti. Sorry.

        ArrayList<BufferedImage> imgs = new ArrayList<BufferedImage>();
        boolean lastColumnEmpty = true;
        int startCol = 0; //start column of split
        int minTop=1000, maxBot=0;  //I'm pretty sure these do nothing right now, which is okay
                                    //because the vertical bounding will be done with scanlines elsewhere
        int area=0;
        for(int x=0;x<img.getWidth();x++){
            boolean columnHasData = false; 
            for(int y=0;y<img.getHeight();y++){
                if((img.getRGB(x,y)&0xffffff) != 0xffffff){
                    if(lastColumnEmpty){
                        startCol = x;
                        area = 0;
                        //We're on a back row
                    }
                    area++;
                    lastColumnEmpty=false;
                    columnHasData=true;
                }else if(!columnHasData && !lastColumnEmpty && y == img.getHeight()-1){
                    //Found two empty columns on either side of data
                    //so split that into a character
                    lastColumnEmpty = true;
                    if(area >= minArea && x-startCol>=minWidth){
                        //only attempt to split an image if the area and width are large enough
                        int ytop;
                        boolean good = false;
                        for(ytop=1;!good && ytop < img.getHeight();ytop++){
                            for(int xx=startCol;!good && xx<=x;xx++){
                                if((img.getRGB(xx,ytop)&0xffffff) == 0){
                                    good=true;
                                }
                            }
                        }
                        ytop--;

                        int ybot;
                        good = false;
                        for(ybot=img.getHeight()-2;!good && ybot >=1;ybot--){
                            for(int xx=startCol;!good && xx<=x;xx++){
                                if((img.getRGB(xx,ybot)&0xffffff) == 0){
                                    good=true;
                                }
                            }
                        }
                        ybot+=2; //not sure why I went with 2 instead of 1...there was some reason...

                        if(ybot-ytop > 2){
                            if((ybot-ytop)*3/2 < x-startCol){
                                //If the image is too wide to logically be a single character, split it up into
                                //posible sub-characters based on the AVG_FONT_WIDTH measurement
                                BufferedImage scimg = scaleImage(img.getSubimage(startCol,ytop,x-startCol,ybot-ytop),-1,24);
                                int col=0;
                                for(int n = scimg.getWidth()/AVG_FONT_WIDTH;n>0;n--){
                                    imgs.add(scimg.getSubimage(col,0,AVG_FONT_WIDTH,24));
                                    col+=AVG_FONT_WIDTH;
                                }
                                if(col<scimg.getWidth()){
                                    //Split the remainder as another character
                                    imgs.add(scimg.getSubimage(col,0,scimg.getWidth()-col,24));
                                }
                            }else
                                imgs.add(img.getSubimage(startCol,ytop,x-startCol,ybot-ytop));
                        }
                    }
                }
            }
        }
        return imgs;
    }


    /**
     * Split a B/W image into its component characters and attempt to read them through OCR
     *
     * @param img image to be split
     * @param match necessary match percentage for a character to be accepted
     * @param minArea minimum number of pixels required for an image segment to be considered a possible character
     * @param minWidth minimum width required for an image segment to be considered a possible character
     * @return string of characters matched
     *
     * @see #splitImage(java.awt.image.BufferedImage, int, int)
     */
    public String processLetters(BufferedImage img, double match, int minArea, int minWidth){
        double prevave=avgMatchPercent;
        avgMatchPercent = 0;
        matchPercentCount = 0;
        matchPercentSum = 0;
        String s = "";
        for(BufferedImage i:splitImage(img,minArea,minWidth))
            s+=getOCRMatch(scaleImage(i,FONT_WIDTH,FONT_HEIGHT),match);
        if(matchPercentCount>0){
            avgMatchPercent= matchPercentSum / matchPercentCount;
        }else{
            avgMatchPercent = prevave;
        }
        return s;
    }

    
    /**
     * Finds the character that best matches <tt>image</tt> with at least a <tt>match</tt> percent match
     * 
     * @param img image of character to be matched
     * @param match minimum match percentage to be considered an "acceptable" match
     * @return the best character match for <tt>image</tt>, or an empty string if no characters matched above <tt>match</tt> percent
     */
    public String getOCRMatch(BufferedImage img,double match){
        double max = 0;
        int index = 0;
        for(int i=0;i<FONT_DATA.length;i++){
            double c = testCharacter(img, FONT_DATA[i]);
            if(c>max){
                max=c;
                index=i;
            }
        }
        if(max < match) return "";
        matchPercentSum+=max;
        matchPercentCount++;
        return FONT_TEXT.substring(index,index+1);
    }


    /**
     * Tests an image of a character against an image of a sample character and returns a match percentage
     * 
     * @param charImg image of character to test
     * @param sampleImg sample character image data
     * @return match percentage in decimal form
     */
    public double testCharacter(BufferedImage charImg,BufferedImage sampleImg){
        if(sampleImg == null)return 0;
        BufferedImage img1 = new BufferedImage(Math.max(charImg.getWidth(),sampleImg.getWidth()),Math.max(charImg.getHeight(),sampleImg.getHeight()),BufferedImage.TYPE_INT_ARGB);
        BufferedImage img2 = new BufferedImage(Math.max(charImg.getWidth(),sampleImg.getWidth()),Math.max(charImg.getHeight(),sampleImg.getHeight()),BufferedImage.TYPE_INT_ARGB);
        img1.createGraphics().drawImage(charImg, 0,0,null);
        img2.createGraphics().drawImage(sampleImg, 0,0,null);
        int[] i1 = img1.getRGB(0,0,img1.getWidth(),img1.getHeight(),null,0,img1.getWidth());
        int[] i2 = img2.getRGB(0,0,img2.getWidth(),img2.getHeight(),null,0,img2.getWidth());
        if(i1.length != i2.length)return 0.0;
        int correct = 0, total = 0;
        for(int i=0;i<Math.min(i1.length,i2.length);i++){
            if(i1[i] == 0xFF000000 || i2[i] == 0xFF000000)total++;
            if(i1[i] == i2[i] && i1[i] == 0xFF000000) correct++;
        }
        if(correct == 0 )return 0.0;
        double percent = (double)correct/(double)total;
        return percent;
    }


    /**
     * Scales an image to new dimensions, preserving aspect ratio as much as possible
     * <p>
     * A value of <tt>neww</tt> or <tt>newh</tt> that is less than 1 will result in that dimension being scaled to fit
     * the aspect ratio of <tt>image</tt>. If both <tt>neww</tt> and <tt>newh</tt> are less than 1, a copy of the
     * original image will be returned.
     *
     * @param image image to be scaled
     * @param neww desired image width
     * @param newh desired image height
     * @return scaled copy of <tt>img</tt> with a white background
     */
    public static BufferedImage scaleImage(BufferedImage image, int neww, int newh){
        if(neww <= 0 && newh <= 0){
            neww=image.getWidth();
            newh=image.getHeight();
        }else{
            float scaleh=1f,scalew=1f;
            if(neww <= 0 || image.getHeight()/image.getWidth()>newh/(float)neww){
                scalew = image.getWidth()/(float)image.getHeight();
            }else{
                scaleh = image.getWidth()/(float)image.getHeight();
            }
            neww=(int)(neww>0?neww:newh*scalew);
            newh=(int)(newh>0?newh:neww*scaleh);
            if(neww==0)neww++;
            if(newh==0)newh++;
        }
        BufferedImage res = new BufferedImage(neww,newh,BufferedImage.TYPE_INT_ARGB);
        res.getGraphics().setColor(Color.WHITE);
        res.getGraphics().fillRect(0, 0, neww, newh);
        res.getGraphics().drawImage(image.getScaledInstance(neww,newh,BufferedImage.SCALE_FAST),0,0,null);
        return res;
    }


    /**
     * Calculates the edit distance between two strings
     *
     * @param s0
     * @param s1
     * @param approx allow characters to match commonly-confused characters for a lower match cost
     * @return edit distance between <tt>s0</tt> and <tt>s1</tt>
     */
    public float LevenshteinDistance (String s0, String s1, boolean approx) {
        int len0 = s0.length()+1;
        int len1 = s1.length()+1;

        // the array of distances
        float[] cost = new float[len0];
        float[] newcost = new float[len0];

        // initial cost of skipping prefix in String s0
        for(int i=0;i<len0;i++) cost[i]=i;

        // dynamicaly computing the array of distances

        // transformation cost for each letter in s1
        for(int j=1;j<len1;j++) {

            // initial cost of skipping prefix in String s1
            newcost[0]=j-1;

            // transformation cost for each letter in s0
            for(int i=1;i<len0;i++) {

                // matching current letters in both strings
                float match = (s0.charAt(i-1)==s1.charAt(j-1)) ? 0 :
                        (approx ? approxCharMatch(s1.charAt(j-1),s0.charAt(i-1)) : 1);

                // computing cost for each transformation
                float cost_replace = cost[i-1]+match;
                float cost_insert  = cost[i]+1;
                float cost_delete  = newcost[i-1]+1;

                // keep minimum cost
                newcost[i] = Math.min(Math.min(cost_insert, cost_delete),cost_replace );
            }

            // swap cost/newcost arrays
            float[] swap=cost; cost=newcost; newcost=swap;
        }

        // the distance is the cost for transforming all letters in both strings
        return cost[len0-1];
    }


    /**
     * Returns the edit distance cost of <tt>b</tt> as a match for <tt>a</tt>
     *
     * @param a
     * @param b
     * @return edit distance cost
     */
    public float approxCharMatch(char a,char b){

        if(a==b)return 0;
        for(int i=0;i<APPROX_SUBS.length;i++){
            if(APPROX_SUBS[i].charAt(0) == a){
                if(APPROX_SUBS[i].substring(1).indexOf(b)>-1){
                    return (APPROX_SUBS[i].length()+APPROX_SUBS[i].substring(1).indexOf(b))/(APPROX_SUBS[i].length() * 2f);
                }else{
                    return 1f;
                }
            }
        }
        return 1f;
    }



    public String matchFloorString(String str){
        String res="";
        float best=1000;
        for(String s:FLOORS){
            float lev=LevenshteinDistance(s,str,true);
            if(lev<best){
                best=lev;
                res=s;
            }
        }
        if(best>res.length()/3)return "";
        return res;
    }

    public float matchFloorDist(String str){
        float best=1000;
        for(String s:FLOORS){
            float lev=LevenshteinDistance(s,str,true);
            if(lev<best){
                best=lev;
            }
        }
        return best;
    }

    public float matchTopBarDist(String str){
        float best=1000;
        for(String s:ITEMS){
            float lev=LevenshteinDistance(s,str,true);
            if(lev<best){
                best=lev;
            }
        }
        return best;
    }

    public String matchItemString(String str){
        String res="";
        float best=1000;
        for(Item i:Item.ITEMS){
            float lev=LevenshteinDistance(i.ocrname.substring(i.ocrname.startsWith("*") || i.ocrname.startsWith("#")?1:0),str,false);
            if(lev<best){
                best=lev;
                res=i.ocrname;
            }
        }
        if(best>res.length()/3 || res.startsWith("#"))return "";
        return res;
    }


    public Item getItemObject(String name){
        float best=1000;
        for(Item i:Item.ITEMS){
            if(i.ocrname.equals(name))
                return i;
        }
        return null;
    }



    //##################################################################################################
    // STATIC FONT DATA AND NAME STRINGS, etc...
    //##################################################################################################


    public static BufferedImage[] loadFont(){
        BufferedImage[] charArray = new BufferedImage[FONT_TEXT.length()];
        try{
            int ptr = 0;
            BufferedImage img = javax.imageio.ImageIO.read(ItemTracker.class.getResource("/items/ISAACFONT2_3.png"));
            boolean backrow = true;
            int scol = 0, tt=1000, bb=0;
            for(int x=0;x<img.getWidth();x++){
                boolean cc = false;
                for(int y=0;!cc && y<img.getHeight();y++){
                    if((img.getRGB(x,y)&0xffffff) != 0xffffff){
                        if(backrow)
                            scol = x;
                        backrow=false;
                        cc = true;
                    }else{
                        if(!backrow && y == img.getHeight()-1){
                            backrow = true;
                            charArray[ptr++]=(scaleImage(img.getSubimage(scol,3,x-scol,24),31,24));
                            //System.out.println(x+", "+y+"     "+Integer.toHexString(img.getRGB(x,y))+"     "+(x-scol)+"    "+(ybot-ytop) + "     ytop = "+ytop + "      ybot = "+ybot);
                        }
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return charArray;
       //return new BufferedImage[FONT_TEXT.length()];
    }
    
    public static final int AVG_FONT_WIDTH = 26;
    public static final int FONT_WIDTH=31;
    public static final int FONT_HEIGHT=24;
    public static final double FONT_RATIO=FONT_WIDTH/(double)FONT_HEIGHT;
    public static final double AVG_FONT_RATIO = AVG_FONT_WIDTH/(double)FONT_HEIGHT;
    public static final BufferedImage[] FONT_DATA = loadFont();
    public static final String FONT_TEXT = "<31UP!9VOLTADOLARONYOUARTERBENCHISCKBOKSFRINBDAGHEMYGTSY-USBHDKDWFMNUFMXVPCMKYIVCVGGPWOWJJZ8X'PW=.JOZ223XE";
    public static final String[] FLOORS = {"BASEMENT1","BASEMENT2","BASEMENTXL","CELLAR1","CELLAR2","CELLARXL","CAVES1","CAVES2","CAVESXL","CATACOMBS1","CATACOMBS2","CATACOMBS","THEDEPTHS1","THEDEPTHS2","THEDEPTHSXL","NECROPOLIS1","NECROPOLIS2","NECROPOLIS","THEWOMB1","THEWOMB2","THEWOMBXL","UTERO1","UTERO2","UTERO","UTEROXL","SHEOL","CATHEDRAL","THECHEST","CHEST"};
    public static final String[] APPROX_SUBS={"1'  ","ARHB  F","B8R G","DO G","ELKF","HARN B","RAH B","P  F","F  P","TFI1","NHA RK","MH","WH","ODU GC","VU ","UV  "};
    public static final String[] ITEMS = {
            "<3",
            "1UP!",
            "9VOLT",
            "ADOLLAR!",
            "*APONY",
            "AOUARTER",
            "ABEL",
            "*ANARCHISTCOOKBOOK",
            "ANKH",
            "*BESTFRIEND",
            "BLOODBAG",
            "BLOODOFTHEMARTYR",
            "*BLOODRIGHTS",
            "BOBBY-BOMB",
            "BOBSCURSE",
            "*BOBSROTTENHEAD",
            "BOMBBAG",
            "*BOOKOFREVELATIONS",
            "*BOOKOFSHADOWS",
            "BOOM!",
            "BREAKFAST",
            "BRIMSTONE",
            "BROTHERBOBBY",
            "BUCKETOFLARD",
            "BUMFRIEND",
            "BOX",
            "CAT-O-NINE-TAILS",
            "CHARMOFTHEVAMPIRE",
            "CHEMICALPEEL",
            "CHOCOLATEMILK",
            "*CRACKTHESKY",
            "*CRYSTALBALL",
            "CUBEOFMEAT",
            "CUPIDSARROW",
            "*DADSKEY",
            "DEADBIRD",
            "DEADCAT",
            "DEADDOVE",
            "*DEADSEASCROLLS",
            "*DECKOFCARDS",
            "DEMONBABY",
            "DESSERT",
            "DINNER",
            "DISTANTADMIRATION",
            "*DOCTORSREMOTE",
            "DRFETUS",
            "EPICFETUS",
            "FATE",
            "FOREVERALONE",
            "*FORGETMENOW",
            "GHOSTBABY",
            "GROWTHHORMONES",
            "GUARDIANANGEL",
            "GUPPYSHAIRBALL",
            "*GUPPYSHEAD",
            "*GUPPYSPAW",
            "GUPPYSTAIL",
            "HABIT",
            "HALOOFFLIES",
            "HARLEOUINBABY",
            "HOLYWATER",
            "INFESTATION",
            "THEINNEREYE",
            "IPECAC",
            "*IVBAG",
            "JESUSJUICE",
            "*KAMIKAZE!",
            "*LEMONMISHAP",
            "LILCHUBBY",
            "LITTLECHAD",
            "LITTLEGISH",
            "LITTLESTEVE",
            "LOKISHORNS",
            "LORDOFTHEPIT",
            "LUCKYFOOT",
            "ALUMPOFCOAL",
            "LUNCH",
            "MAGIC8BALL",
            "MAGICMUSHROOM",
            "MAGNETO",
            "MAX'SHEAD",
            "MEAT!",
            "MINIMUSH",
            "*MOMSBOTTLEOFPILLS",
            "*MOMSBRA",
            "MOMSCOINPURSE",
            "MOMSCONTACTS",
            "MOMSEYE",
            "MOMSHEELS",
            "MOMSKNIFE",
            "MOMSLIPSTICK",
            "*MOMSPAD",
            "MOMSPURSE",
            "MOMSUNDERWEAR",
            "MONEY=POWER",
            "*MONSTERMANUEL",
            "*MONSTROSTOOTH",
            "*MR.BOOM!",
            "MRMEGA",
            "THEMULLIGAN",
            "MUTANTSPIDER",
            "*MYLITTLEUNICORN",
            "MYREFLECTION",
            "*NOTCHEDAXE",
            "NUMBERONE",
            "ODDMUSHROOM",
            "ODDMUSHROOMTHIN",
            "OUIJABOARD",
            "PAGEANTBOY",
            "PENTAGRAM",
            "PHD",
            "POLYPHEMUS",
            "*PORTABLESLOT!",
            "*PRAYERCARD",
            "PYRO",
            "BLOODYLUST",
            "RAINBOWBABY",
            "RAWLIVER",
            "*RAZORBLADE",
            "*REMOTEDETONATOR",
            "ROBO-BABY",
            "ROIDRAGE",
            "ROSARY",
            "ROTTENMEAT",
            "SACKOFPENNIES",
            "SACREDHEART",
            "SACRIFICIALDAGGER",
            "THESADONION",
            "SCAPULAR",
            "*SHOOPDAWHOOP!",
            "SISTERMAGGY",
            "SKATOLE",
            "SKELETONKEY",
            "SMBSUPERFAN!",
            "SPEEDBALL",
            "SPELUNKERHAT",
            "SPIDERBITE",
            "*SPIDERBUTT",
            "SPIRITOFTHENIGHT",
            "SPOONBENDER",
            "SOUEEZY",
            "STEAMSALE",
            "STEMCELLS",
            "STEVEN",
            "STIGMATA",
            "SUPERBANDAGE",
            "*TAMMYSHEAD",
            "TECHNOLOGY",
            "TECHNOLOGY2",
            "*TELEPATHYFORDUMMIES",
            "*TELEPORT!",
            "THEBATTERY",
            "*THEBEAN",
            "THEBELT",
            "*THEBIBLE",
            "THEBLACKBEAN",
            "*THEBOOKOFBELIAL",
            "*THEBOOKOFSIN",
            "*THECANDLE",
            "CELTICCROSS",
            "THECOMMONCOLD",
            "THECOMPASS",
            "*D2O",
            "*THEDICE",
            "DADDYLONGLEGS",
            "*THEGAMEKID",
            "THEHALO",
            "HOLYGRAIL",
            "*THEHOURGLASS",
            "THELADDER",
            "THEMARK",
            "MITRE",
            "*THENAIL",
            "*THENECRONOMICON",
            "THEPACT",
            "THEPARASITE",
            "THEPEEPER!",
            "*THEPINKINGSHEARS",
            "*THEPOOP!",
            "THERELIC",
            "THESMALLROCK",
            "THEVIRUS",
            "THEWAFER",
            "*WHITEPONY",
            "3DOLLARBILL",
            "TOOTHPICKS",
            "TOUGHLOVE",
            "TRANSCENDENCE",
            "TREASUREMAP",
            "*WENEEDTOGODEEPER!",
            "WHOREOFBABYLON",
            "WIGGLEWORM",
            "WIRECOATHANGER!",
            "WOODENSPOON",
            "XRAY-VISION",
            "*YUMHEART",
            "#PILLSHERE!",
            "#TAROTCARD",
            "#ISAACSHEAD",
            "#FISHHEAD",
            "#PINKYEYE",
            "#PUSHPIN",
            "#LIBERTYCAP",
            "#UMBILICALCORD",
            "#CHILDSHEART",
            "#CURVEDHORN",
            "#RUSTEDKEY",
            "#GOATHOOF",
            "#MOMSPEARL",
            "#CANCER",
            "#REDPATCH",
            "#MATCHSTICK",
            "#LUCKYTOE",
            "#CURSEDSKULL",
            "#SAFETYCAP",
            "#ACEOFSPADES",
            "#ISAACSFORK",
            "#THEPOLAROID",
            "#AMISSINGPAGE",
            "#BLOODYPENNY",
            "#BURNTPENNY",
            "#FLATPENNY",
            "#COUNTERFEITPENNY",
            "#TICK",
            "#ISAACSHEAD",
            "#MAGGYSFAITH",
            "#JUDAS'TONGUE",
            "#???'SSOUL",
            "#SAMSONSLOCK",
            "#CAINSEYE",
            "#EVESBIRDFOOT",
            "#THELEFTHAND"
    };


    /**
     * Used to pass a custom match function into a routine
     */
    interface ColorMatch {

        /**
         *
         *
         * @param color
         * @return
         */
        public double match(int color);
    }

}